//
//  Copyright (C) 2014 Tom Beckmann
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

using Clutter;
using Meta;

namespace Gala.Plugins.Notify
{
	/**
	 * Wrapper class only containing the summary and body label. Allows us to
	 * instantiate the content very easily for when we need to slide the old
	 * and new content down.
	 */
	class NormalNotificationContent : Actor
	{
		static Regex entity_regex;
		static Regex tag_regex;

		static construct
		{
			try {
				entity_regex = new Regex ("&(?!amp;|quot;|apos;|lt;|gt;)");
				tag_regex = new Regex ("<(?!\\/?[biu]>)");
			} catch (Error e) {}
		}

		const int LABEL_SPACING = 2;

		Text summary_label;
		Text body_label;

		construct
		{
			summary_label = new Text.with_text (null, "");
			summary_label.line_wrap = true;
			summary_label.use_markup = true;
			summary_label.line_wrap_mode = Pango.WrapMode.WORD_CHAR;

			body_label = new Text.with_text (null, "");
			body_label.line_wrap = true;
			body_label.use_markup = true;
			body_label.line_wrap_mode = Pango.WrapMode.WORD_CHAR;

			var style_path = new Gtk.WidgetPath ();
			style_path.append_type (typeof (Gtk.Window));
			style_path.append_type (typeof (Gtk.EventBox));
			style_path.iter_add_class (1, "gala-notification");
			style_path.append_type (typeof (Gtk.Label));

			var label_style_context = new Gtk.StyleContext ();
			label_style_context.add_provider (Notification.default_css, Gtk.STYLE_PROVIDER_PRIORITY_FALLBACK);
			label_style_context.set_path (style_path);

			Gdk.RGBA color;

			label_style_context.save ();
			label_style_context.add_class ("title");
			color = label_style_context.get_color (Gtk.StateFlags.NORMAL);
			summary_label.color = {
				(uint8) (color.red * 255),
				(uint8) (color.green * 255),
				(uint8) (color.blue * 255),
				(uint8) (color.alpha * 255)
			};
			label_style_context.restore ();

			label_style_context.save ();
			label_style_context.add_class ("label");
			color = label_style_context.get_color (Gtk.StateFlags.NORMAL);
			body_label.color = {
				(uint8) (color.red * 255),
				(uint8) (color.green * 255),
				(uint8) (color.blue * 255),
				(uint8) (color.alpha * 255)
			};
			label_style_context.restore ();

			add_child (summary_label);
			add_child (body_label);
		}

		public void set_values (string summary, string body)
		{
			summary_label.set_markup ("<b>%s</b>".printf (fix_markup (summary)));
			body_label.set_markup (fix_markup (body));
		}

		public override void get_preferred_height (float for_width, out float min_height, out float nat_height)
		{
			float label_height;
			get_allocation_values (null, null, null, null, out label_height, null);

			min_height = nat_height = label_height;
		}

		public override void allocate (ActorBox box, AllocationFlags flags)
		{
			float label_x, label_width, summary_height, body_height, label_height, label_y;
			get_allocation_values (out label_x, out label_width, out summary_height,
				out body_height, out label_height, out label_y);

			var summary_alloc = ActorBox ();
			summary_alloc.set_origin (label_x, label_y);
			summary_alloc.set_size (label_width, summary_height);
			summary_label.allocate (summary_alloc, flags);

			var body_alloc = ActorBox ();
			body_alloc.set_origin (label_x, label_y + summary_height + LABEL_SPACING);
			body_alloc.set_size (label_width, body_height);
			body_label.allocate (body_alloc, flags);

			base.allocate (box, flags);
		}

		void get_allocation_values (out float label_x, out float label_width, out float summary_height,
			out float body_height, out float label_height, out float label_y)
		{
			var height = Notification.ICON_SIZE;

			label_x = Notification.MARGIN + Notification.PADDING + height + Notification.SPACING;
			label_width = Notification.WIDTH - label_x - Notification.MARGIN - Notification.SPACING;

			summary_label.get_preferred_height (label_width, null, out summary_height);
			body_label.get_preferred_height (label_width, null, out body_height);

			label_height = summary_height + LABEL_SPACING + body_height;
			label_y = Notification.MARGIN + Notification.PADDING;
			// center
			if (label_height < height) {
				label_y += (height - (int) label_height) / 2;
				label_height = height;
			}
		}

		/**
		 * Copied from gnome-shell, fixes the mess of markup that is sent to us
		 */
		string fix_markup (string markup)
		{
			var text = markup;

			try {
				text = entity_regex.replace (markup, markup.length, 0, "&amp;");
				text = tag_regex.replace (text, text.length, 0, "&lt;");
			} catch (Error e) {}

			return text;
		}
	}

	public class NormalNotification : Notification
	{
		public string summary { get; construct set; }
		public string body { get; construct set; }
		public uint32 sender_pid { get; construct; }
		public string[] notification_actions { get; construct set; }
		public Screen screen { get; construct; }

		Actor content_container;
		NormalNotificationContent notification_content;
		NormalNotificationContent? old_notification_content = null;

		public NormalNotification (Screen screen, uint32 id, string summary, string body, Gdk.Pixbuf? icon,
			NotificationUrgency urgency, int32 expire_timeout, uint32 pid, string[] actions)
		{
			Object (
				id: id,
				icon: icon,
				urgency: urgency,
				expire_timeout: expire_timeout,
				screen: screen,
				summary: summary,
				body: body,
				sender_pid: pid,
				notification_actions: actions
			);
		}

		construct
		{
			content_container = new Actor ();

			notification_content = new NormalNotificationContent ();
			notification_content.set_values (summary, body);

			content_container.add_child (notification_content);
			insert_child_below (content_container, null);
		}

		public void update (string summary, string body, Gdk.Pixbuf? icon, int32 expire_timeout,
			string[] actions)
		{
			var visible_change = this.summary != summary || this.body != body;

			if (visible_change) {
				if (old_notification_content != null)
					old_notification_content.destroy ();

				old_notification_content = new NormalNotificationContent ();
				old_notification_content.set_values (this.summary, this.body);

				content_container.add_child (old_notification_content);

				this.summary = summary;
				this.body = body;
				notification_content.set_values (summary, body);

				float content_height, old_content_height;
				notification_content.get_preferred_height (0, null, out content_height);
				old_notification_content.get_preferred_height (0, null, out old_content_height);

				content_height = float.max (content_height, old_content_height);

				play_update_transition (content_height + PADDING * 2);

				get_transition ("switch").completed.connect (() => {
					if (old_notification_content != null)
						old_notification_content.destroy ();
					old_notification_content = null;
				});
			}

			notification_actions = actions;
			update_base (icon, expire_timeout);
		}

		protected override void update_slide_animation ()
		{
			if (old_notification_content != null)
				old_notification_content.y = animation_slide_y_offset;

			notification_content.y = animation_slide_y_offset - animation_slide_height;
		}

		public override void update_allocation (out float content_height, AllocationFlags flags)
		{
			var box = ActorBox ();
			box.set_origin (0, 0);
			box.set_size (width, height);

			content_container.allocate (box, flags);

			// the for_width is not needed in our implementation of get_preferred_height as we
			// assume a constant width
			notification_content.get_preferred_height (0, null, out content_height);

			content_container.set_clip (MARGIN, MARGIN, MARGIN * 2 + WIDTH, content_height + PADDING * 2);
		}

		public override void get_preferred_height (float for_width, out float min_height, out float nat_height)
		{
			float content_height;
			notification_content.get_preferred_height (for_width, null, out content_height);

			min_height = nat_height = content_height + (MARGIN + PADDING) * 2;
		}

		public override void activate ()
		{
			// we currently only support the default action, which can be triggered by clicking
			// on the notification according to spec
			for (var i = 0; i < notification_actions.length; i += 2) {
				if (notification_actions[i] == "default") {
					action_invoked (id, "default");
					dismiss ();

					return;
				}
			}

			// if no default action has been set, we fallback to trying to find a window for the
			// notification's sender process
			var window = get_window ();
			if (window != null) {
				var workspace = window.get_workspace ();
				var time = screen.get_display ().get_current_time ();

				if (workspace != screen.get_active_workspace ())
					workspace.activate_with_focus (window, time);
				else
					window.activate (time);

				dismiss ();
			}
		}

		Window? get_window ()
		{
			if (sender_pid == 0)
				return null;

			foreach (var actor in Compositor.get_window_actors (screen)) {
				var window = actor.get_meta_window ();

				// the windows are sorted by stacking order when returned
				// from meta_get_window_actors, so we can just pick the first
				// one we find and have a pretty good match
				if (window.get_pid () == sender_pid)
					return window;
			}

			return null;
		}

		void dismiss ()
		{
			closed (id, NotificationClosedReason.DISMISSED);
			close ();
		}
	}
}

